package am.aua.renju.bussinesslayer;

import am.aua.renju.bussinesslayer.exception.GameDuplicationException;
import am.aua.renju.bussinesslayer.exception.InvalidInputException;
import am.aua.renju.bussinesslayer.gamelogic.GameLogic;
import am.aua.renju.bussinesslayer.gamelogic.GameLogic.GameLogicState;
import am.aua.renju.bussinesslayer.gamelogic.PlayData;
import am.aua.renju.databaseaccess.GameOperations;
import am.aua.renju.databaseaccess.IGameOperations;
import am.aua.renju.databaseaccess.IUserOperations;
import am.aua.renju.databaseaccess.UserOperations;
import am.aua.renju.databaseaccess.util.GameState;
import am.aua.renju.entity.Game;
import am.aua.renju.entity.Player;
import java.util.Random;
import java.util.TreeMap;

/**
 *
 * @author ara
 */
public class GameManager {

    private static GameManager instance;
    private TreeMap<Integer, GameLogic> logicOfGames; //all the ongoing games logic    
    private IGameOperations gameOperations;
    private IUserOperations userOperations;

    public static GameManager getInstance() {
        if (instance == null) {
            instance = new GameManager();
        }
        return instance;
    }

    private GameManager() {
        logicOfGames = new TreeMap<Integer, GameLogic>();
        gameOperations = GameOperations.getInstance();
        userOperations = UserOperations.getInstance();
    }

    public boolean createGame(String email, boolean startRule)
            throws GameDuplicationException {
        return createGame(email, startRule, 0);
    }

    public boolean createGame(String email, boolean startRule, int time)
            throws GameDuplicationException {
        Game game = new Game();
        if (isGameDuplication(email, startRule, time)) {
            throw new GameDuplicationException();
        }
        //game.setId(1); //JPA requirement for persisting new game. Even though in database it is autoincrement.
        game.setPlayer1Id(userOperations.getUser(email));
        game.setStartRules((short) (startRule ? 1 : 0));
        if (time != 0) {
            game.setTime(time);
        }
        game.setState(GameState.WAIT.toString());
        gameOperations.createGame(game);
        if (isGameNumberLimitReach(email)) {
            return true;
        }
        return false;
    }

    public MoveData opponentPlay(String email) {
        Player player = userOperations.getUser(email);
        Game game = gameOperations.getPlayGame(player);
        GameLogic gameLogic = logicOfGames.get(game.getId());
        if (gameLogic != null) {
            PlayData lastMove = gameLogic.getLastMove();
            if (lastMove != null) {
                boolean isPlayerWhite = playerIsWhite(player, game);
                if (lastMove.gameLogicState == GameLogicState.WHITE_WON) {
                    logicOfGames.remove(game.getId());
                    gameOperations.updateGameState(game.getId(), game.getPlayer1Id().getEmail().equals(player.getEmail()) ? GameState.PLAYER1_WON : GameState.PLAYER2_WON);
                    Player secondPlayer = game.getPlayer1Id().getEmail().equals(player.getEmail()) ? game.getPlayer2Id() : game.getPlayer1Id();
                    double[] eloResult = calcElo(1, player.getRating(), secondPlayer.getRating());
                    userOperations.updateUserRating(email, (int) eloResult[0]);
                    userOperations.updateUserRating(secondPlayer.getEmail(), (int) eloResult[1]);
                } else if (lastMove.gameLogicState == GameLogicState.BLACK_WON) {
                    logicOfGames.remove(game.getId());
                    gameOperations.updateGameState(game.getId(), game.getPlayer1Id().getEmail().equals(player.getEmail()) ? GameState.PLAYER1_WON : GameState.PLAYER2_WON);
                    Player secondPlayer = game.getPlayer1Id().getEmail().equals(player.getEmail()) ? game.getPlayer2Id() : game.getPlayer1Id();
                    double[] eloResult = calcElo(2, player.getRating(), secondPlayer.getRating());
                    userOperations.updateUserRating(email, (int) eloResult[0]);
                    userOperations.updateUserRating(secondPlayer.getEmail(), (int) eloResult[1]);
                } else if (lastMove.gameLogicState == GameLogic.GameLogicState.DRAW) {
                    logicOfGames.remove(game.getId());
                    gameOperations.updateGameState(game.getId(), game.getPlayer1Id().getEmail().equals(player.getEmail()) ? GameState.PLAYER1_WON : GameState.PLAYER2_WON);
                    Player secondPlayer = game.getPlayer1Id().getEmail().equals(player.getEmail()) ? game.getPlayer2Id() : game.getPlayer1Id();
                    double[] eloResult = calcElo(0, player.getRating(), secondPlayer.getRating());
                    userOperations.updateUserRating(email, (int) eloResult[0]);
                    userOperations.updateUserRating(secondPlayer.getEmail(), (int) eloResult[1]);
                }
                if (isPlayerWhite) {
                    if (lastMove.isWhite == false) {
                        return new MoveData(lastMove.x, lastMove.y, lastMove.gameLogicState);
                    }
                } else {
                    if (lastMove.isWhite == true) {
                        return new MoveData(lastMove.x, lastMove.y, lastMove.gameLogicState);
                    }
                }
            }
        }
        return null;
    }

    public NewGameInfo joinGame(String fromNick, int time, boolean startRule, String joinedPlayerEmail) throws InvalidInputException {
        Random rand = new Random();
        Player player1 = userOperations.getUserByNickname(fromNick);
        Player player2 = userOperations.getUser(joinedPlayerEmail);
        Game game;
        if (time != 0) {
            game = gameOperations.getWaitGame(player1, time, startRule ? (short) 1 : (short) 0);
        } else {
            game = gameOperations.getWaitGame(player1, startRule ? (short) 1 : (short) 0);
        }
        if (game == null) {
            throw new InvalidInputException();
        }
        short isWhite = (short) rand.nextInt(1);
        gameOperations.updateGameToJoin(game.getId(), player2, isWhite);
        NewGameInfo joinGameReturn;
        if (isWhite == 1) {
            joinGameReturn = new NewGameInfo(fromNick, player2.getNickname());
        } else {
            joinGameReturn = new NewGameInfo(player2.getNickname(), fromNick);
        }
        return joinGameReturn;
    }

    public void deleteGame(String email, int time, boolean startRule) {
        Player user = userOperations.getUser(email);
        gameOperations.deleteGame(user, time, startRule ? (short) 1 : (short) 0);
    }

    public NewGameInfo joinGameHost(String hostplayerEmail) {
        Player hostUser = userOperations.getUser(hostplayerEmail);
        Game game = gameOperations.getPlayGame(hostUser);
        if (game == null) {
            return null;
        }
        if (game.getIsPlayer1White() == 1) {
            return new NewGameInfo(hostUser.getNickname(), game.getPlayer2Id().getNickname());
        } else {
            return new NewGameInfo(game.getPlayer2Id().getNickname(), hostUser.getNickname());
        }
    }

    public boolean joinRequest(String hostPlayerEmail) {
        Player hostUser = userOperations.getUser(hostPlayerEmail);
        Game game = gameOperations.getPlayGame(hostUser);
        if (game == null) {
            return false;
        } else {
            return true;
        }
    }

    public GameLogicState play(String email, int x, int y) {
        Player player = userOperations.getUser(email);
        Game game = gameOperations.getPlayGame(player);
        GameLogic gameLogic = logicOfGames.get(game.getId());
        if (gameLogic == null) {
            logicOfGames.put(game.getId(), new GameLogic());
            gameLogic = logicOfGames.get(game.getId());
        }
        GameLogicState gameLogicState;
        if (playerIsWhite(player, game)) {
            gameLogicState = gameLogic.playWhite(x, y);

        } else {
            gameLogicState = gameLogic.playBlack(x, y);
        }
        return gameLogicState;
    }

    // Returns an array of two doubles: [0] is the new rating for player A, [1] for player B  
    //  
    // Parameters:  
    //   status: 0 = draw, 1 = player A won, 2 = player B won  
    //   RA, RB: the players points before the game  
    private double[] calcElo(int status, double RA, double RB) {
        double QA = Math.pow(10, RA / 400);
        double QB = Math.pow(10, RB / 400);

        // set the desired k factor  
        double k = 32;

        double EA = QA / (QA + QB);
        double EB = QB / (QA + QB);

        double SA = 0.5; // draw  
        double SB = 0.5; // draw  
        if (status == 1) {
            SA = 1;
            SB = 0;
        } else if (status == 2) {
            SA = 0;
            SB = 1;
        }

        double[] res = new double[2];
        res[0] = RA + (k * (SA - EA));
        res[1] = RB + (k * (SB - EB));
        return res;
    }

    public GameLogic startGame(int gameId) {
        gameOperations.updateGameState(gameId, GameState.PLAY);
        return new GameLogic();
    }

    public void clearCreatedGames(String userEmail) {
        Player user = userOperations.getUser(userEmail);
        gameOperations.deleteGames(user);
    }

    public boolean playerIsWhite(String email) {
        Player user = userOperations.getUser(email);
        Game game = gameOperations.getPlayGame(user);
        return playerIsWhite(user, game);
    }

    private boolean playerIsWhite(Player user, Game game) {
        if (game.getPlayer1Id().getEmail().equals(user.getEmail())) {
            return game.getIsPlayer1White() == 1;
        } else {
            return game.getIsPlayer1White() == 0;
        }
    }

    private boolean isGameNumberLimitReach(String email) {
        Player player1 = userOperations.getUser(email);
        if (gameOperations.getGames(player1).size() > 4) {
            return true;
        }
        return false;
    }

    private boolean isGameDuplication(String email, boolean startRule, int time) {
        Player player1 = userOperations.getUser(email);
        if (time == 0) {
            if (gameOperations.getWaitGame(player1, startRule ? (short) 1 : (short) 0) != null) {
                return true;
            }
        }
        if (gameOperations.getWaitGame(player1, time, startRule ? (short) 1 : (short) 0) != null) {
            return true;
        }
        return false;
    }

    public void joinGame(String email) {
    }
}
